Skip to content

feat: add swap_api FastAPI service for browser swaps (PR #1 of browser-swap roadmap)#311

Open
LandynDev wants to merge 2 commits into
testfrom
feat/swap-api
Open

feat: add swap_api FastAPI service for browser swaps (PR #1 of browser-swap roadmap)#311
LandynDev wants to merge 2 commits into
testfrom
feat/swap-api

Conversation

@LandynDev
Copy link
Copy Markdown
Collaborator

Summary

Implements PR #1 of the browser-swap roadmap: a stateless FastAPI service inside the `allways` package that wraps the existing CLI swap modules with HTTP for browser orchestration. Per spec docs/swap-api/browser-swap-spec.md §6 — one new service, zero contract / DB / das-allways changes.

  • HTTP wrapper at `allways/swap_api/`: `/healthz`, `/chains`, `/miners`, `/miners/best`, `/proofs/reserve`, `/proofs/confirm`, `POST /reserve`, `POST /confirm`.
  • Reuses `contract_client`, `commitments`, `dendrite_lite`, `synapses`, `utils.proofs`, `utils.rate` directly — no duplicated logic.
  • `POST /reserve` re-reads the live miner commitment, exact-match guards `expectedRate`, returns `409 RateChanged` on any drift (spec §4 zero-tolerance).
  • Validator broadcasts go only to the contract whitelist via `discover_validators(contract_client=...)` (spec §4 — covered by `test_reserve_passes_contract_client_to_validator_discovery`).
  • Added `broadcast_synapse_async` to `dendrite_lite` so FastAPI handlers don't own their event loop; sync `broadcast_synapse` now wraps the async path (CLI behavior unchanged).
  • New pure helper `allways/utils/rate_selection.py` factors the CLI's eligibility filter and rate ranking out of `cli/swap_commands/swap.py` so both surfaces stay in sync.

Spec deviation

`POST /confirm` body adds `minerHotkey` alongside `requestHash`. The contract has no requestHash→miner index, and the browser already gets `minerHotkey` back from `POST /reserve`. Routes it directly into the `SwapConfirmSynapse.reservation_id` field the validators expect. `requestHash` is kept as a correlation field.

Implementation notes

  • `build_app_state` reads `WS_ENDPOINT` / `NETUID` / `CONTRACT_ADDRESS` at startup; ephemeral wallet cached once via `get_ephemeral_wallet()` (spec gotcha).
  • Polling cadence + quorum timeout overridable via `SWAP_API_QUORUM_TIMEOUT_S` / `SWAP_API_QUORUM_POLL_S`.
  • CORS open public (`GET/POST/OPTIONS`).
  • Reuses `render_and_aggregate` from `validator_rejections` with a buffer-backed `rich.Console` to keep the CLI's translated-rejection logic without console output.

Test plan

  • Lint clean: `ruff format . && ruff check .`
  • Unit tests: `pytest tests/swap_api/` — 15 tests, all pass
  • Full repo tests: `pytest tests/` — 426 tests, all pass (no CLI regression)
  • Smoke import: `python -c "from allways.swap_api.app import create_app; create_app()"` enumerates all 8 spec routes
  • E2E against live dev environment — see PR Cap max fee at 5% to prevent collateral rug via fee manipulation #3 (`alw-utils`) for the curl-based suite that exercises the HTTP path end-to-end

Roadmap

This is PR #1 of 4 in docs/swap-api/pr-roadmap.md. PR #2 (`allways-ui` Swap page) and PR #3 (`alw-utils` dev-env + E2E suite) both depend on this merging first.

Stateless HTTP front-end for browser swaps — wraps the existing
contract_client / commitments / dendrite_lite modules with the eight
endpoints from docs/swap-api/browser-swap-spec.md §6. Holds no keys,
no funds, no state; broadcasts SwapReserveSynapse / SwapConfirmSynapse
to whitelisted validators and waits for on-chain quorum.

POST /reserve guards expectedRate exactly against the live commitment;
any drift returns 409 RateChanged so the UI can re-quote. Adds an
async broadcast helper in dendrite_lite so FastAPI handlers don't have
to manage their own event loop, and a pure rate_selection module that
factors the CLI's miner-eligibility logic out of swap.py.
@xiao-xiao-mao xiao-xiao-mao Bot added the feature Net-new functionality label May 11, 2026
bt.Dendrite owns an aiohttp ClientSession that __del__ cannot close
cleanly outside a running loop. Per-request instantiation in the
async path was leaking sockets under sustained load. Wrap the call
in 'async with' so the session is torn down with each broadcast.

CLI's sync wrapper still goes through this path via its own
event loop, behavior unchanged.

Review surfaced in cross-repo review of feat/swap-api.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant